本文也发布在公司论坛中
内容概要:对于Android线程所属的CGroup,一些资料认为是通过线程优先级来进行划分的。但经实践,发现部分Android版本与该观点并不一致,那么实际情况又是怎样的呢?本篇文章就该问题进行了探讨。

一、CGroup简要介绍
在Linux中,不同线程分配cpu时间片的策略首先是基于线程优先级的,线程优先级越高,越容易分配到cpu。但是这样就产生了低优先级线程一直都被抢占cpu时间的问题,为解决该问题,Linux 2.6.23版本中引入了CFS策略,该策略不但会参考单个线程的优先级,还会追踪每个线程已经获取到的time slice数量,如果高优先级的线程已经执行了很长时间,但低优先级的线程一直在等待,后续系统会保证低优先级的线程也能获取更多的CPU时间。这就产生了新的问题:优先级高的线程并不一定总能在争取时间片上有绝对的优势,反映在Android中,就会出现UI线程被后台线程抢占cpu时间的问题。所以在Linux 2.6.24中又引入了CGroup的概念,让从属于特定CGroup的线程能够占据更多的时间片而不被低线程抢占,从而提升了总体效率[1]。

二、Android中不同版本所属CGroup调研
在Android中,存在两类特别重要的CGroup,一类是foreground group,UI线程属于这一类。另一类是background group,工作线程属于这一类。那么你可能要问,线程所属foreground group和background group到底怎样划分呢?查阅API文档以及相关资料[2],当我们使用setThreadPriority,nice值大于等于THREAD_PRIORITY_BACKGROUND将属于background group,其余属于foreground group。
但是,这种说法正确嘛?让我们来做个实验,首先通过以下代码设置线程优先级。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
private static final ThreadFactory sDBThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);

public Thread newThread(@NonNull Runnable r) {
WorkerRunnable wr = new WorkerRunnable(r, Process.THREAD_PRIORITY_BACKGROUND);
return new Thread(wr, "Async DB Thread #" + mCount.getAndIncrement());
}
};

private static class WorkerRunnable implements Runnable {
Runnable runnable;
int priority;
String tag;

public WorkerRunnable(Runnable runnable, int priority) {
this(runnable, priority, "WorkerRunnable");
}

public WorkerRunnable(Runnable runnable, int priority, String tag) {
this.runnable = runnable;
this.priority = priority;
this.tag = tag;
}

@Override
public void run() {
android.os.Process.setThreadPriority(priority);
runnable.run();
}

@Override
public String toString() {
return tag + ":" + runnable;
}
}

在Android5.1.0中当线程优先级为Process.THREAD_PRIORITY_BACKGROUND的Async DB Thread所属的进程处于前台时,其cgroup却为fg,当所属进程进入到后台时,其cgroup为bg

CGroup-1

CGroup-2

而在Android 4.1中,无论是否处于前台,其cgroup都为bg

CGroup-3

看来,实际情况是和版本有关,那么线程的cgroup是具体怎样设置的呢?查阅相关源代码[3],找到是在
sched_policy.c的get_sched_policy方法进行相关设置的,这里以Android4.1为例,其他版本逻辑基本一致,除了Android7.0[4],增加了一层宏定义判断#ifdef USE_CPUSETS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
int get_sched_policy(int tid, SchedPolicy *policy)
{

#ifdef HAVE_GETTID
if (tid == 0) {
tid = gettid();
}
#endif
pthread_once(&the_once, __initialize);
if (__sys_supports_schedgroups) {
char grpBuf[32];
if (getSchedulerGroup(tid, grpBuf, sizeof(grpBuf)) < 0)
return -1;
if (grpBuf[0] == '\0') {
*policy = SP_SYSTEM;
} else if (!strcmp(grpBuf, "apps/bg_non_interactive")) {
*policy = SP_BACKGROUND;
} else if (!strcmp(grpBuf, "apps")) {
*policy = SP_FOREGROUND;
} else {
errno = ERANGE;
return -1;
}
} else {
int rc = sched_getscheduler(tid);
if (rc < 0)
return -1;
else if (rc == SCHED_NORMAL)
*policy = SP_FOREGROUND;
else if (rc == SCHED_BATCH)
*policy = SP_BACKGROUND;
else {
errno = ERANGE;
return -1;
}
}
return 0;
}

上述代码基本逻辑概要如下:

CGroup-4

该方法首先查看变量__sys_supports_schedgroups是否为true,

1
2
3
if (!access("/dev/cpuctl/tasks", F_OK)) {
__sys_supports_schedgroups = 1;
}

也就是查看/dev/cpuctl/tasks是否存在:当/dev/cpuctl/tasks存在时,access返回0,满足条件__sys_supports_schedgroups为1。

那么Android5.1满足什么样的条件呢?实际上,在Android5.1的init.rc[5]中会创建了该tasks文件,但4.1.1[6]不存在该文件。那么5.1.0系统就使用getSchedulerGroup得到cgroup,也就是查找/proc/线程id/cgroup文件中的2:cpu:/一行的内容,
当整个进程在前台时,其2:cpu:的值为空,而处于后台时,则为bg_non_interactive。

CGroup-5

而/proc//cgroup 文件中的2:cpu:的值又是怎样写进去的呢?参考Linux源码cgroup.c中的处理[7]和相关资料[8],发现创建的子线程也是同样用的所属进程的cgroup。

接下来是Android4.1.1,他直接使用sched_getscheduler,也就是我们之前sched_setscheduler设置的policy:

1
sched_setscheduler(tid, (policy == SP_BACKGROUND) ? SCHED_BATCH : SCHED_NORMAL, &param);

而sched_setscheduler则是由set_sched_policy调用,

1
2
3
4
5
6
7
8
9
if (gDoSchedulingGroup) {
if (prio >= ANDROID_PRIORITY_BACKGROUND) {
set_sched_policy(androidGetTid(), SP_BACKGROUND);
} else if (prio > ANDROID_PRIORITY_AUDIO) {
set_sched_policy(androidGetTid(), SP_FOREGROUND);
} else {
// defaults to that of parent, or as set by requestPriority()
}
}

也就是通过线程的优先级来设置所属CGroup。

上面提到的/dev/cpuctl/tasks和在Android7.0中引入的/dev/cpuset/等文件的创建在init.rc中进行定义[5],不同的版本有不同的定义,这里查阅不同版本的源码,做一下Android不同版本的CGroup的实际情况的总结:
Android 4.0 Ice Cream Sandwich,根据线程优先级设置cgroup[9]
Android 4.1/4.2/4.3 Jelly Bean,根据线程优先级设置cgroup[6]
Android 4.4 KitKat,根据线程优先级设置cgroup[10]
Android 5.0/5.1 Lollipop,取所属进程的cgroup值[5]
Android 6.0 Marshmallow,取所属进程的cgroup值[11]
Android 7.0 Nougat,取所属进程的cgroup值[12]

参考资料
[1]http://coolshell.cn/articles/17049.html
[2]http://www.androiddesignpatterns.com/2014/01/thread-scheduling-in-android.html
[3]https://android.googlesource.com/platform/system/core/+/jb-release/libcutils/sched_policy.c
[4]https://android.googlesource.com/platform/system/core/+/nougat-release/libcutils/sched_policy.c
[5]https://android.googlesource.com/platform/system/core/+/lollipop-mr1-release/rootdir/init.rc
[6]https://android.googlesource.com/platform/system/core/+/jb-release/rootdir/init.rc
[7]https://github.com/torvalds/linux/blob/master/kernel/cgroup.c
[8]http://www.infoq.com/cn/articles/docker-kernel-knowledge-cgroups-resource-isolation
[9]https://android.googlesource.com/platform/system/core/+/ics-mr0-release/rootdir/init.rc
[10]https://android.googlesource.com/platform/system/core/+/kitkat-release/rootdir/init.rc
[11]https://android.googlesource.com/platform/system/core/+/marshmallow-release/rootdir/init.rc
[12]https://android.googlesource.com/platform/system/core/+/nougat-release/rootdir/init.rc